/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package edu.mit.csail.sdg.alloy4;

import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Map;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLConnection;
import java.awt.Color;
import java.awt.Dimension;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;

/** This class asks the user for permission to email a bug report when an uncaught exception occurs. */

public final class MailBug implements UncaughtExceptionHandler, Runnable {

   /** The version number of the most recent Alloy4 (as queried from alloy.mit.edu); -1 if alloy.mit.edu has not replied yet. */
   private static int latestAlloyVersion = -1;

   /** The name of the most recent Alloy4 (as queried from alloy.mit.edu); "unknown" if alloy.mit.edu has not replied yet. */
   private static String latestAlloyVersionName = "unknown";

   /** The URL where the bug report should be sent. */
   private static final String ALLOY_URL = "http://alloy.mit.edu/postbug4.php";

   /** The URL where the current version info can be queried. */
   private static final String ALLOY_NOW = "http://alloy.mit.edu/alloy4/download/alloy4.txt";

   /** If alloy.mit.edu has replied, then return the latest Alloy build number, else return -1. */
   public static int latestBuildNumber() { synchronized(MailBug.class) { return latestAlloyVersion; } }

   /** If alloy.mit.edu has replied, then return the latest Alloy build name, else return "unknown" */
   public static String latestBuildName() { synchronized(MailBug.class) { return latestAlloyVersionName; } }

   /** Construct a new MailBug object. */
   private MailBug() { }

   /** Setup the uncaught-exception-handler and use a separate thread to query alloy.mit.edu for latest version number. */
   public static void setup() {
      if (Thread.getDefaultUncaughtExceptionHandler() != null) return;
      MailBug x = new MailBug();
      Thread.setDefaultUncaughtExceptionHandler(x);
      new Thread(x).start();
   }

   /** This method concatenates a Throwable's message and stack trace and all its causes into a single String. */
   public static String dump (Throwable ex) {
      StringBuilder sb = new StringBuilder();
      while(ex != null) {
         sb.append(ex.getClass()).append(": ").append(ex.getMessage()).append('\n');
         StackTraceElement[] trace = ex.getStackTrace();
         if (trace!=null) for(int n=trace.length, i=0; i<n; i++) sb.append(trace[i]).append('\n');
         ex = ex.getCause();
         if (ex != null) sb.append("caused by...\n");
      }
      return sb.toString().trim();
   }

   /** This method returns true if the exception appears to be a Sun Java GUI bug. */
   private static boolean isGUI(Throwable ex) {
      Throwable cause = ex.getCause();
      if (cause != null && !isGUI(cause)) return false;
      StackTraceElement[] trace = ex.getStackTrace();
      for(int n=(trace==null ? 0 : trace.length), i=0; i<n; i++) {
         String name = trace[i].getClassName();
         if (!name.startsWith("java.") && !name.startsWith("javax.") && !name.startsWith("sun.")) return false;
      }
      return true;
   }

   /** This method prepares the crash report. */
   private static String prepareCrashReport (Thread thread, Throwable ex, String email, String problem) {
      StringWriter sw = new StringWriter();
      PrintWriter pw = new PrintWriter(sw);
      pw.printf("Alloy Analyzer %s crash report (Build Date = %s)\n", Version.version(), Version.buildDate());
      pw.printf("\n========================= Email ============================\n%s\n", Util.convertLineBreak(email).trim());
      pw.printf("\n========================= Problem ==========================\n%s\n", Util.convertLineBreak(problem).trim());
      pw.printf("\n========================= Thread Name ======================\n%s\n", thread.getName().trim());
      if (ex!=null) pw.printf("\n========================= Stack Trace ======================\n%s\n", dump(ex));
      pw.printf("\n========================= Preferences ======================\n");
      try {
         for(String key: Preferences.userNodeForPackage(Util.class).keys()) {
            String value = Preferences.userNodeForPackage(Util.class).get(key, "");
            pw.printf("%s = %s\n", key.trim(), value.trim());
         }
      } catch(BackingStoreException bse) {
         pw.printf("BackingStoreException occurred: %s\n", bse.toString().trim());
      }
      pw.printf("\n========================= System Properties ================\n");
      pw.println("Runtime.freeMemory() = " + Runtime.getRuntime().freeMemory());
      pw.println("nRuntime.totalMemory() = " + Runtime.getRuntime().totalMemory());
      for(Map.Entry<Object,Object> e: System.getProperties().entrySet()) {
         String k = String.valueOf(e.getKey()), v = String.valueOf(e.getValue());
         pw.printf("%s = %s\n", k.trim(), v.trim());
      }
      pw.printf("\n========================= The End ==========================\n\n");
      pw.close();
      sw.flush();
      return sw.toString();
   }

   /** This method opens a connection then read the entire content (it converts non-ASCII into '?'); if error occurred it returns "".
    * @param URL - the remote URL we want to read from
    * @param send - if nonempty we will send it to the remote URL before attempting to read from the remote URL
    */
   private static String readAll(String URL, String send, String failure) {
      BufferedInputStream bis = null;
      InputStream in = null;
      OutputStream out = null;
      String ans;
      try {
         URLConnection connection = new URL(URL).openConnection();
         if (send!=null && send.length() > 0) {
            connection.setDoOutput(true);
            out = connection.getOutputStream();
            out.write(send.getBytes("UTF-8"));
            out.close();
            out = null;
         }
         in = connection.getInputStream();
         bis = new BufferedInputStream(in);
         StringBuilder sb = new StringBuilder();
         int i;
         while((i = bis.read()) >= 0) { sb.append((char)(i<=0x7F ? i : '?')); }
         ans = Util.convertLineBreak(sb.toString());
      } catch (Throwable ex) {
         ans = failure;
      } finally {
         Util.close(bis);
         Util.close(in);
         Util.close(out);
      }
      return ans;
   }

   /** This method will query alloy.mit.edu for the latest version number. */
   public void run() {
      String result = readAll(ALLOY_NOW + "?buildnum=" + Version.buildNumber() + "&builddate=" + Version.buildDate(), "", "");
      if (!result.startsWith("Alloy Build ")) return;
      // Now that we know we're online, try to remove the old ill-conceived "Java WebStart" versions of Alloy4 BETA1..BETA7
      Subprocess.exec(20000, new String[]{
            "javaws", "-silent", "-offline", "-uninstall", "http://alloy.mit.edu/alloy4/download/alloy4.jnlp"});
      // Now parse the result
      int num = 0;
      boolean found = false;
      for(int i=0, len=result.length(); ; i++) {
         if (i >= len) return; // malformed
         char c = result.charAt(i);
         if (!(c>='0' && c<='9')) { if (!found) continue; else { result = result.substring(i).trim(); break; } }
         found = true;
         num = num*10 + (c - '0');
      }
      synchronized(MailBug.class) { latestAlloyVersionName = result;  latestAlloyVersion = num; }
   }

   /** This method sends the crash report then displays alloy.mit.edu's reply in a text window. */
   private static void sendCrashReport (Thread thread, Throwable ex, String email, String problem) {
      try {
         final String report = prepareCrashReport(thread, ex, email, problem);
         final String alt = "Sorry. An error has occurred in posting the bug report.\n"
            + "Please email this report to alloy@mit.edu directly.\n\n" + dump(ex);
         final JTextArea status = OurUtil.textarea("Sending the bug report... please wait...",
               10, 40, false, true, new LineBorder(Color.GRAY));
         new Thread(new Runnable() {
            public void run() {
               final String output = readAll(ALLOY_URL, report, alt);
               SwingUtilities.invokeLater(new Runnable() {
                  public void run() {
                     status.setText(output);
                     status.setCaretPosition(0);
                  }
               });
            }
         }).start();
         OurDialog.showmsg("Sending the bug report... please wait...", status);
      } finally {
         System.exit(1);
      }
   }

   /** This method is an exception handler for uncaught exceptions. */
   public void uncaughtException (Thread thread, Throwable ex) {
      if (isGUI(ex)) return;
      final int ver;
      final String name;
      synchronized(MailBug.class) { ver = latestAlloyVersion; name = latestAlloyVersionName; }
      if (ex!=null) {
         System.out.flush();
         System.err.flush();
         System.err.println("Exception: " + ex.getClass());
         System.err.println("Message: " + ex);
         System.err.println("Stacktrace:");
         System.err.println(dump(ex));
         System.err.flush();
      }
      final String yes = "Send the Bug Report", no = "Don't Send the Bug Report";
      final JTextField email = OurUtil.textfield("", 20, new LineBorder(Color.DARK_GRAY));
      final JTextArea problem = OurUtil.textarea("", 50, 50, true, false, new EmptyBorder(0,0,0,0));
      final JScrollPane scroll = OurUtil.scrollpane(problem, new LineBorder(Color.DARK_GRAY), new Dimension(300, 200));
      for(Throwable ex2 = ex; ex2 != null; ex2 = ex2.getCause()) {
         if (ex2 instanceof StackOverflowError) OurDialog.fatal(new Object[] {
               "Sorry. The Alloy Analyzer has run out of stack space.",
               " ",
               "Try simplifying your model or reducing the scope.",
               "And try reducing Options->SkolemDepth to 0.",
               "And try increasing Options->Stack.",
               " ",
               "There is no way for Alloy to continue execution, so pressing OK will shut down Alloy."
         });
         if (ex2 instanceof OutOfMemoryError) OurDialog.fatal(new Object[] {
               "Sorry. The Alloy Analyzer has run out of memory.",
               " ",
               "Try simplifying your model or reducing the scope.",
               "And try reducing Options->SkolemDepth to 0.",
               "And try increasing Options->Memory.",
               " ",
               "There is no way for Alloy to continue execution, so pressing OK will shut down Alloy."
         });
      }
      if (ver > Version.buildNumber()) OurDialog.fatal(new Object[] {
            "Sorry. A fatal error has occurred.",
            " ",
            "You are running Alloy Analyzer " + Version.version(),
            "but the most recent is Alloy Analyzer "+ name,
            " ",
            "Please try to upgrade to the newest version",
            "as the problem may have already been fixed.",
            " ",
            "There is no way for Alloy to continue execution, so pressing OK will shut down Alloy."
      });
      if (OurDialog.yesno(new Object[] {
            "Sorry. A fatal internal error has occurred.",
            " ",
            "You may submit a bug report (via HTTP).",
            "The error report will include your system",
            "configuration, but no other information.",
            " ",
            "If you'd like to be notified about a fix,",
            "please describe the problem and enter your email address.",
            " ",
            OurUtil.makeHT("Email:", 5, email, null),
            OurUtil.makeHT("Problem:", 5, scroll, null)
      }, yes, no)) sendCrashReport(thread, ex, email.getText(), problem.getText());
      System.exit(1);
   }
}
